iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0
Software Development

讓 C# 也可以很 Social - 在 .NET 6 用 C# 串接 LINE Services API 的取經之路系列 第 24

[Day 24] .NET 6 C# 與 Line Services API 開發 - Line Pay 串接 (一)

  • 分享至 

  • xImage
  •  
tags: .NET6 C#, LineBot, Line Messaging API, C#, dotnet core

[Day 24] 讓 C# 也能很 Social - .NET 6 C# 與 Line Services API 開發 - Line Pay 串接 (一)

前言

今天這篇的內容會帶大家完成一個基本的 Line Pay 交易流程,內容包含前端與後端。

Line Pay

以下是 Line Pay 大致的付款流程

每筆 Line Pay 交易有以下狀態 :

  1. 剛建立交易請求 (尚未得到使用者的交易授權)
  2. 使用者登入並選擇付款方式等等資訊後授權交易 (已授權,等待確認交易)
  3. 使用者/商家 server 送出確認交易,並請款 (完成交易)

上述第 3 點的狀態可另外再將確認交易請款分成兩步驟,變成以下 4 種狀態 :

  1. 剛建立交易請求 (尚未得到使用者的交易授權)
  2. 使用者登入並選擇付款方式等等資訊後授權交易 (已授權,等待確認交易)
  3. 使用者/商家 server 送出確認交易 (等待請款)
  4. 商家送出請款請求(完成交易)

對應以上的幾種狀態, Line Pay 提供了以下 5 支主要的 API

  1. Request : 送出交易請求 (在此處設定此交易是否要將請款狀態獨立出來)。
  2. Confirm : 確認交易。
  3. Capture : 請款請求。
  4. Void : 將已取得使用者授權的交易取消。
  5. Refund : 對已完成請款(交易完成) 的交易進行退款。

而我們今天建立的基本交易流程以 1、2 為主,剩下的我們下一篇再說明~

接下來,要使用 Line Pay API,則必須有 Line Pay 商家的帳號,並在其中取得 Channel Id & Channe Secret Key,在開發測試的過程中,可以使用 Line 提供的 sandbox 的環境,接下來先去註冊一個 snadbox 帳號吧。(* 正式的LINE PAY帳號需要跟官方申請,需要比較多的文件佐證,詳細就留給各位參考官方的文件囉)

註冊 Line Pay Sandbox

跟著以下步驟註冊並取得 channel id & channel secret key

  1. 註冊 sandbox 帳號 Line Pay Sandbox 註冊連結

  2. 註冊成功後,會在 email 收到 sandbox 的帳號密碼。

  3. 前往 Line Pay 商家頁面使用剛剛收到的帳密登入。 Line Pay 商家

4.登入成功後到 管理連結金鑰 目錄中,按下查詢並取得驗證碼驗證成功後即可取得 channel id & channel secret key (此為安全機制,並不是每次都需要重新驗證查詢取得)

取得 channel id 與 channel secret key 後就可以開始串接 Line Pay API 了~

建立基本付款流程

下圖是我們這次要建立的流程圖,包含前端與後端。

  • 下圖這是每個 Line Pay API Request 需帶入的 Header,其中有兩項比較特別
    • X-LINE-Authorization-Nonce : UUID
    • X-LINE-Authorization : 還需要 HMAC Base64 Signature

所以在建立 API 前先建立一個 SignatureProvider

建立 SignatureProvider (HMAC-SHA256) Line Pay 文件連結

* 若對 HMAC-SHA256 有興趣,介紹可以參考這篇 連結 *

Line Pay 使用 HMAC-SHA256 演算法做簽章,又根據要打的 API 不同的 Http Method 有不同的訊息內容要求。

看起來雖然很複雜,但 .NET 有提供 HMAC-SHA256 的方法,直接使用即可。

  • 在 Providers 資料夾新增 SignatureProvider.cs
using System.Security.Cryptography;

namespace LineBotMessage.Providers
{
    public static class SignatureProvider
    {

        public static string HMACSHA256(string key, string message)
        {
            System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();

            //取的 key byte 值
            byte[] keyByte = encoding.GetBytes(key);

            // 取得 key 對應的 hmacsha256
            HMACSHA256 hmacsha256 = new HMACSHA256(keyByte);

            // 取的 message byte 值
            byte[] messageBytes = encoding.GetBytes(message);

            // 將 message 使用 key 值對應的 hamcsha256 作 hash 簽章
            byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);

            return Convert.ToBase64String(hashmessage);
        }
    }
}

LinePayService

using System.Net.Http.Headers;
using System.Text;
using System.Web;
using LineBotMessage.Dtos;
using LineBotMessage.Providers;

namespace LineBotMessage.Domain
{
    public class LinePayService
    {
        public LinePayService()
        {
            client = new HttpClient();
            _jsonProvider = new JsonProvider();
        }

        private readonly string channelId = "{你的 LinePay 商家 Channel ID}";
        private readonly string channelSecretKey = "{你的 Line Pay 商家 Channel Secret Key}";


        private readonly string linePayBaseApiUrl = "https://sandbox-api-pay.line.me";

        private static HttpClient client;
        private readonly JsonProvider _jsonProvider;
    }
}

建立付款請求 (Request) 文件連結

(文件內容太長,就不截圖了,class 將文件上所有屬性都建立起來了,但這邊測試不會全部用到,各位事後自行測試吧!)

建立 Class

  • 建立 PaymentRequestDto.cs,這裡是建立交易請求時用到的 class
namespace LineBotMessage.Dtos
{
    public class PaymentRequestDto
    {
        public int Amount { get; set; }
        public string Currency { get; set; }
        public string OrderId { get; set; }
        public List<PackageDto> Packages { get; set; }
        public RedirectUrlsDto RedirectUrls { get; set; }
        public RequestOptionDto? Options { get; set; }
    }
    public class PackageDto
    {
        public string Id { get; set; }
        public int Amount { get; set; }
        public string Name { get; set; }
        public List<LinePayProductDto> Products { get; set; }
        public int? UserFee { get; set; }

    }
    public class LinePayProductDto
    {
        public string Name { get; set; }
        public int Quantity { get; set; }
        public int Price { get; set; }
        public string? Id { get; set; }
        public string? ImageUrl { get; set; }
        public int? OriginalPrice { get; set; }
    }

    public class RedirectUrlsDto
    {
        public string ConfirmUrl { get; set; }
        public string CancelUrl { get; set; }
        public string? AppPackageName { get; set; }
        public string? ConfirmUrlType { get; set; }
    }

    public class RequestOptionDto
    {
        public PaymentOptionDto? Payment { get; set; }
        public DisplpyOptionDto? Displpy { get; set; }
        public ShippingOptionDto? Shipping { get; set; }
        public ExtraOptionsDto? Extra { get; set; }
    }
    public class PaymentOptionDto
    {
        public bool? Capture { get; set; }
        public string? PayType { get; set; }
    }
    public class DisplpyOptionDto
    {
        public string? Local { get; set; }
        public bool? CheckConfirmUrlBrowser { get; set; }
    }
    public class ShippingOptionDto
    {
        public string? Type { get; set; }
        public int FeeAmount { get; set; }
        public string? FeeInquiryUrl { get; set; }
        public string? FeeInquiryType { get; set; }
        public ShippingAddressDto? Address { get; set; }
    }

    public class ShippingAddressDto
    {
        public string? Country { get; set; }
        public string? PostalCode { get; set; }
        public string? State { get; set; }
        public string? City { get; set; }
        public string? Detail { get; set; }
        public string? Optional { get; set; }
        public ShippingAddressRecipientDto Recipient { get; set; }
    }

    public class ShippingAddressRecipientDto
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        public string? FirstNameOptional { get; set; }
        public string? LastNameOptional { get; set; }
        public string? Email { get; set; }
        public string? PhoneNo { get; set; }
        public string? Type { get; set; }
    }

    public class ExtraOptionsDto
    {
        public string? BranchName { get; set; }
        public string? BranchId { get; set; }
    }
}
  • 建立 PaymentRequestResponseDto.cs,這是建立交易後收到的回覆 class
namespace LineBotMessage.Dtos
{
    public class PaymentConfirmResponseDto
    {
        public string ReturnCode { get; set; }
        public string ReturnMessage { get; set; }
        public ConfirmResponseInfoDto Info { get; set; }
    }

    public class ConfirmResponseInfoDto
    {
        public string OrderId { get; set; }
        public long TransactionId { get; set; }
        public string AuthorizationExpireDate { get; set; }
        public string RegKey { get; set; }
        public ConfirmResponsePayInfoDto[] PayInfo { get; set; }
    }

    public class ConfirmResponsePayInfoDto
    {
        public string Method { get; set; }
        public int Amount { get; set; }
        public string CreditCardNickname { get; set; }
        public string CreditCardBrand { get; set; }
        public string MaskedCreditCardNumber { get; set; }
        public ConfirmResponsePackageDto[] Packages { get; set; }
        public ConfirmResponseShippingOptionsDto Shipping { get; set; }
    }
    public class ConfirmResponsePackageDto
    {
        public string Id { get; set; }
        public int Amount { get; set; }
        public int UserFeeAmount { get; set; }
    }
    public class ConfirmResponseShippingOptionsDto
    {
        public string MethodId { get; set; }
        public int FeeAmount { get; set; }
        public ShippingAddressDto Address { get; set; }
    }

}

LinePayService 新增 function

  • 在 LinePayService 中新增 SendPaymentRequest function
// 送出建立交易請求至 Line Pay Server
public async Task<PaymentResponseDto> SendPaymentRequest(PaymentRequestDto dto)
{
    var json = _jsonProvider.Serialize(dto);
    // 產生 GUID Nonce
    var nonce = Guid.NewGuid().ToString();
    // 要放入 signature 中的 requestUrl
    var requestUrl = "/v3/payments/request";

    //使用 channelSecretKey & requestUrl & jsonBody & nonce 做簽章
    var signature = SignatureProvider.HMACSHA256(channelSecretKey, channelSecretKey + requestUrl + json + nonce);

    var request = new HttpRequestMessage(HttpMethod.Post, linePayBaseApiUrl + requestUrl)
    {
        Content = new StringContent(json, Encoding.UTF8, "application/json")
    };
    // 帶入 Headers
    client.DefaultRequestHeaders.Add("X-LINE-ChannelId", channelId);
    client.DefaultRequestHeaders.Add("X-LINE-Authorization-Nonce", nonce);
    client.DefaultRequestHeaders.Add("X-LINE-Authorization", signature);

    var response = await client.SendAsync(request);
    var linePayResponse = _jsonProvider.Deserialize<PaymentResponseDto>(await response.Content.ReadAsStringAsync());

    Console.WriteLine(nonce);
    Console.WriteLine(signature);

    return linePayResponse;
}    

確認付款請求 (Confirm) 文件連結

建立 Class

  • 建立 PaymentConfirmDto.cs
namespace LineBotMessage.Dtos
{
    public class PaymentConfirmDto
    {
        public int Amount { get; set; }
        public string Currency { get; set; }
    }
}
  • 建立 PaymentConfirmResponseDto.cs
namespace LineBotMessage.Dtos
{
    public class PaymentResponseDto
    {
        public string ReturnCode { get; set; }
        public string ReturnMessage { get; set; }
        public ResponseInfoDto Info { get; set; }
    }

    public class ResponseInfoDto
    {
        public ResponsePaymentUrlDto PaymentUrl { get; set; }
        public long TransactionId { get; set; }
        public string PaymentAccessToken { get; set; }
    }

    public class ResponsePaymentUrlDto
    {
        public string Web { get; set; }
        public string App { get; set; }
    }
}

LinePayService 新增 function

  • 在 LinePayService 中新增 SendPaymentRequest function
// 取得 transactionId 後進行確認交易
public async Task<PaymentConfirmResponseDto> ConfirmPayment(string transactionId, string orderId, PaymentConfirmDto dto) //加上 OrderId 去找資料
{
    var json = _jsonProvider.Serialize(dto);

    var nonce = Guid.NewGuid().ToString();
    var requestUrl = string.Format("/v3/payments/{0}/confirm", transactionId);
    var signature = SignatureProvider.HMACSHA256(channelSecretKey, channelSecretKey + requestUrl + json + nonce);

    var request = new HttpRequestMessage(HttpMethod.Post, String.Format(linePayBaseApiUrl + requestUrl, transactionId))
    {
        Content = new StringContent(json, Encoding.UTF8, "application/json")
    };

    client.DefaultRequestHeaders.Add("X-LINE-ChannelId", channelId);
    client.DefaultRequestHeaders.Add("X-LINE-Authorization-Nonce", nonce);
    client.DefaultRequestHeaders.Add("X-LINE-Authorization", signature);

    var response = await client.SendAsync(request);
    var responseDto = _jsonProvider.Deserialize<PaymentConfirmResponseDto>(await response.Content.ReadAsStringAsync());
    return responseDto;
}

交易取消 API

當使用者在交易 Processing 時取消交易,LinePay 會將該資訊透過最開始建立交易時帶入的 CancelUrl 回傳通知,這邊就先開個接口放著就好。

LinePayServicea 新增 function

public async void TransactionCancel(string transactionId)
{
    //使用者取消交易則會到這裏。
    Console.WriteLine($"訂單 {transactionId} 已取消");
}

LinePayController

  • 建立 LinePayController
using LineBotMessage.Dtos;
using LineBotMessage.Domain;
using Microsoft.AspNetCore.Mvc;

namespace LineBotMessage.Controllers
{
    [ApiController]
    [Route("api/[Controller]")]
    public class LinePayController : ControllerBase
    {
        private readonly LinePayService _linePayService;
        public LinePayController()
        {
            _linePayService = new LinePayService();
        }

        [HttpPost("Create")]
        public async Task<PaymentResponseDto> CreatePayment(PaymentRequestDto dto)
        {
            return await _linePayService.SendPaymentRequest(dto);
        }

        [HttpPost("Confirm")]
        public async Task<PaymentConfirmResponseDto> ConfirmPayment([FromQuery] string transactionId, [FromQuery] string orderId, PaymentConfirmDto dto )
        {
            return await _linePayService.ConfirmPayment(transactionId, orderId,dto);
        }

        [HttpGet("Cancel")]
        public async void CancelTransaction([FromQuery] string transactionId)
        {
            _linePayService.TransactionCancel(transactionId);
        }
    }
}

前端畫面

本篇前端有兩個頁面,分別為 products.html & confirm.html,各自代表 :

  1. profucts.html : 商品結帳頁面
  2. confirm.html : 為求測試效果,所以建立此頁進行交易的 confirm

程式碼

  • products.html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>2022 iThome 鐵人賽 - 讓 C# 也能很 Social</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- jquery CDN include -->
    <script src="https://code.jquery.com/jquery-3.6.1.min.js"
        integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
    <!-- CSS include -->
    <link rel="stylesheet" href="style.css">

</head>

<body>
    <script>
        let baseLoginPayUrl = 'https://localhost:8080/api/LinePay/';
        function requestPayment() {
            // 交易訂單假資料
            payment = {
                amount: 3998,
                currency: "TWD",
                orderId: Date.now().toString(), //使用 Timestamp 當作 orderId
                packages: [
                    {
                        id: "20191011I001",
                        amount: 3998,
                        name: "測試",
                        products: [
                            {
                                name: "測試商品",
                                imageUrl: "https://static.accupass.com/org/2011051025162614811630.jpg",
                                quantity: 2,
                                price: 1999,
                            }
                        ]
                    },
                ],
                RedirectUrls: {
                    ConfirmUrl: "https://cccf-61-63-154-173.jp.ngrok.io/confirm.html",
                    CancelUrl: "https://c4f0-61-63-154-173.jp.ngrok.io/api/LinePay/Cancel",
                },
            };

            // 送出 交易申請至商家 server
            $.post({
                url: baseLoginPayUrl + "Create",
                dataType: "json",
                contentType: "application/json",
                data: JSON.stringify(payment),
                success: (res) => {
                    window.location = res.info.paymentUrl.web;
                },
                error: (err) => {
                    console.log(err);
                }
            })
        }
    </script>
    <!-- 最上方的 bar -->
    <div class="topnav">
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/login.html">Line Login</a>
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/profile.html">User Profile</a>
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/products.html">Line Pay</a>
    </div>
    <!-- 商品畫面 -->
    <center>
        <table>
            <thead>
                <tr>
                    <th> 測試商品 </th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td><img src="https://static.accupass.com/org/2011051025162614811630.jpg"></td>
                </tr>
                <tr>
                    <td> 價格 : 1999 </td>
                </tr>
                <tr>
                    <td> 購買數量 : 2 </td>
                </tr>
                <tr>
                    <td style="text-align: right;"> 總金額 : 3998 </td>
                </tr>
                <tr>
                    <td align="center"><button onclick="requestPayment()"> Line Pay 付款</button></td>
                </tr>
            </tbody>
        </table>
    </center>
</body>

</html>
  • confirm.html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>2022 iThome 鐵人賽 - 讓 C# 也能很 Social</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- jquery CDN include -->
    <script src="https://code.jquery.com/jquery-3.6.1.min.js"
        integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
    <!-- CSS include -->
    <link rel="stylesheet" href="style.css">

</head>

<body>
    <script>
        let baseLoginPayUrl = 'https://localhost:8080/api/LinePay/';
        let transactionId = "";
        let orderId = "";

        window.onload = () => {
            // 取出 query parameter 中的 transactionId & orderId
            const params = new Proxy(new URLSearchParams(window.location.search), {
                get: (searchParams, prop) => searchParams.get(prop),
            });

            transactionId = params.transactionId;
            orderId = params.orderId;
        }

        function confirmPayment() {
            // 交易訂單假資料
            payment = {
                amount: 3998,
                currency: "TWD",
            };
            //  送出確認交易
            $.post({
                url: baseLoginPayUrl + `Confirm?transactionId=${transactionId}&orderId=${orderId}`,
                dataType: "json",
                contentType: "application/json",
                data: JSON.stringify(payment),
                success: (res) => {
                    $("#paymentStatus").text("交易狀態 : 成功")
                    console.log(res);
                    
                    setTimeout(() => {
                        window.location = "https://cccf-61-63-154-173.jp.ngrok.io/products.html";
                    }, 2000);
                },
                error: (err) => {
                    console.log(err);
                }
            })
        }
    </script>
    <!-- 最上方的 bar -->
    <div class="topnav">
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/login.html">Line Login</a>
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/profile.html">User Profile</a>
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/products.html">Line Pay</a>
    </div>
    <center>
        <table>
            <thead>
                <tr>
                    <th colspan="2"> 測試商品 </th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td colspan="2"><img src="https://static.accupass.com/org/2011051025162614811630.jpg"></td>
                </tr>
                <tr>
                    <td colspan="2"> 價格 : 1999 </td>
                </tr>
                <tr>
                    <td colspan="2"> 購買數量 : 2 </td>
                </tr>
                <tr>
                    <td colspan="2" style="text-align: right;"> 總金額 : 3998 </td>
                </tr>
                <tr>
                    <td align="center" colspan="2"><button onclick="confirmPayment()"> 確認付款</button></td>
                </tr>
            </tbody>
        </table>
        
        <div class="Container">
            <a id="paymentStatus">交易狀態 : 交易已授權,等待確認<a>
        </div>
    </center>
</body>

</html>

測試

* 因為 ngrok 跨域請求問題的原因,所以前端的 request url 是打 localhost,因此測試只能在電腦上進行

  1. 商品結帳畫面,按下付款後跳轉至 Line Pay 登入畫面

  2. Line Pay 登入畫面

  3. Line Pay 使用者授權模擬畫面,按下付款鈕後即進入授權程序

  4. Confrim 頁面,按下確認後則交易完成。

結語

今天的的內容是只一個基本的付款流程,方便各位參考。
整個流程其實還可以有更多的變化,例如最開始提到的,一種是將確認交易與請款兩個狀態拆開來處理,第二種是可以將 confirm 這件事直接交給 server 負責(讓使用者可省略一步驟)。 除此之外,Line Pay 也有自動扣款的功能可以實作,相信透過今天的例子,大家應該對LINE PAY的使用及相關情境,能有多一點的了解。如果有什麼想多了解的,歡迎再留言讓我們知道囉 ~

下一篇會接續LINE PAY的主題,針對常見情境會用到的API來進行示範 ~

範例程式碼

如果想要參考今天範例程式碼的部份,下面是 Git Repo 連結,方便大家參考。


上一篇
[Day 23] .NET 6 C# 與 Line Services API 開發 - LIFF v2 實作
下一篇
[Day 25] .NET 6 C# 與 Line Services API 開發 - Line Pay 串接 (二)
系列文
讓 C# 也可以很 Social - 在 .NET 6 用 C# 串接 LINE Services API 的取經之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
u2002020021
iT邦新手 5 級 ‧ 2023-04-21 09:44:31

Display 打錯,打成 Displpy ~

PaymentRequestDto.cs
`
public class RequestOptionDto
{
public PaymentOptionDto? Payment { get; set; }
public DisplpyOptionDto? Displpy { get; set; }
public ShippingOptionDto? Shipping { get; set; }
public ExtraOptionsDto? Extra { get; set; }
}

public class DisplpyOptionDto
{
public string? Local { get; set; }
public bool? CheckConfirmUrlBrowser { get; set; }
}

`

我要留言

立即登入留言